
function Template(variables, tokens, original){
	this.variables = variables;
	this.tokens = tokens;
	this.original = original;
}
Template.prototype.render = function(bindings){
	var result = '';
	for(var ti = 0; ti < this.tokens.length; ti++){
		var token = this.tokens[ti];
		if(token.type == 'variable'){
			result += bindings.get(token.value);
			continue;
		} 
		if(token.type == 'function'){
			var tFunc = token.value;
			var parameters = [];
			for(var vi = 0; vi < tFunc.variables.length; vi++){
				parameters.push(bindings.get(tFunc.variables[vi]));
			}
			result += tFunc.func(parameters);
			continue;
		} 
		if(token.type == 'text'){
			result += token.value; continue;
		} 
	}
	
	return result;
}

function TemplateVariable(contextName, name){
	this.contextName = contextName; 
	this.name = name;
	this.completeName = (contextName === undefined ? name : contextName+'.'+name);
}
function TemplateFunction(variables, func){
	this.variables = variables; 
	this.func = func;
}

function TemplateExpression(expressions){
	this.expressions = expressions; 
}
TemplateExpression.prototype.match = function(str){
	var result = {};
	var tokens = str.split(' ');
	if(tokens.length != this.expressions.length) return undefined;
	for(var tci = 0; tci < tokens.length; tci++){
		var expr = this.expressions[tci];
		var token = tokens[tci];
		
		var isVarIndex = expr.length - 4;
		var isVar = (expr.indexOf('_VAR') == isVarIndex);
		if(expr.indexOf('$') == 0){
			if(isVar)result[expr.substring(1, isVarIndex)] = aup.variable(token);
				else result[expr.substring(1)] = token;
		} else {
			if(token != expr) return undefined;
		}
		
	}
	
	return result;
} 
function DataContext(node){
	this.node = node;
}
DataContext.prototype.put = function(name, value){
	this.node.data('data')[name] = value;
}
DataContext.prototype.remove = function(name){
	delete this.node.data('data')[name];
}
DataContext.prototype.forChild = function(childNode){
	for(var key in this.node.data('data')){
		childNode.data('data')[key] = this.node.data('data')[key];
	}
	return new DataContext(childNode)
}

function Bindings(context, extras){
	this.context = context;
	this.extras = extras;
	
	this.contextBindings = {};
	this.extrasBindings = {};
}

Bindings.prototype.get = function(variable){
	
	var contextName = variable.contextName;
	
	if(!contextName) {
		return this.context[variable.name];
	} else {
		if(this.extras[contextName] == undefined) return undefined;
		return this.extras[contextName][variable.name];
	}
} 

Bindings.prototype.set = function(variable, val){
	var contextName = variable.contextName;
	
	if(!contextName) {
		this.context[variable.name] = val;
	} else {
		if(this.extras[contextName] == undefined) throw new Error('Invalid subcontext ' + contextName);
		this.extras[contextName][variable.name]  = val;
		
	}
	
} 
Bindings.prototype.createChild = function(newExtras){
	return new Bindings(this.context, newExtras);
} 

Bindings.prototype.bind = function(node, variable, dataContext){
	var contextName = variable.contextName;
	
	var bindings = this;
	
	/*
	 * 		QUESTA SOLUZIONE FA SCHIFISSIMO!!!
	 *      ATTACCARE I NODI AL CONTEXT FA CAGARE!
	 *      DIPENDENZE CIRCOLARI SE VAI A FARE IL JSON.stringify(data); 
	 * 
	 * 
	 */
	if(!contextName) {
		
		if(!this.context.bindings) this.context.bindings = {};
		
		if(!this.contextBindings[variable.name]){
			this.contextBindings[variable.name] = [node];
			
			var contextBindings = this.contextBindings;
			var renderAgain = function() {
				var bindNodes = contextBindings[variable.name]; 
				for(var ni = 0; ni < bindNodes.length; ni++){
					var n = bindNodes[ni];
					aup.util.render(n, bindings, dataContext);
				}
				
			};
			aup.watch(this.context, variable.name, renderAgain);
		} else {
			this.contextBindings[variable.name].push(node);
		}
		
	} else {
		
		if(this.extras[contextName] === undefined) return;
			
		
		if(!this.extrasBindings[contextName]) this.extrasBindings[contextName] = {};  
		
		if(!this.extrasBindings[contextName][variable.name]){
			this.extrasBindings[contextName][variable.name] = [node];
			
			var extrasBindings = this.extrasBindings;
			var renderAgain = function() {
				var bindNodes = extrasBindings[contextName][variable.name]; 
				for(var ni = 0; ni < bindNodes.length; ni++){
					var n = bindNodes[ni];
					aup.util.render(n, bindings, dataContext);
				}
					
			};
			aup.watch(this.extras[contextName], variable.name, renderAgain);
		} else {
			this.extrasBindings[contextName][variable.name].push(node);
		}
		
	}
	
	
	
} 


aup.start = function(context) {
	
	aup.util.importDirectives();
	
	var elem = aup.byTag('body')[0];
	aup.util.compile(elem);
	
	var dataContext = new DataContext(elem);
	var bindings = new Bindings(context, {});
	aup.util.render(elem, bindings, dataContext);
	

};


aup.util.directives = [];
aup.util.internal_directives = [];

aup.directive = function(name, directive){
	aup.util.directives[name] = directive;
}
aup.variable = function(val){
	return aup.util.parseVariable(val);
}
aup.util.internal_directive = function(name, directive){
	aup.util.internal_directives[name] = directive;
}


aup.util.templateAttributes = ['id', 'name', 'src', 'href'];


aup.util.importDirectives = function(){
	aup.util.internal_directive('internal_text', {
		bindings: function(node, compilation, bindings, dataContext){
			var template = compilation.template;
			for(var ti = 0; ti < template.variables.length; ti++){
				var variable = template.variables[ti];
				bindings.bind(node, variable, dataContext);
			}
		},
		render: function(node, compilation, bindings, dataContext){
			var template = compilation.template;
			var rendered = template.render(bindings);
			node.text(rendered);
		}
	});
	aup.util.internal_directive('internal_attr', {
		bindings: function(node, compilation, bindings, dataContext){
			var template = compilation.template;
			for(var ti = 0; ti < template.variables.length; ti++){
				var variable = template.variables[ti];
				bindings.bind(node, variable, dataContext);
			}
		},
		render: function(node, compilation, bindings, dataContext){
			var template = compilation.template;
			var attr = compilation.attr;
			var rendered = template.render(bindings);
			node.attr(attr, rendered);
		}
	});
	aup.util.internal_directive('internal_class', {
		bindings: function(node, compilation, bindings, dataContext){
			var classTemplate = compilation.template;
			for(var ti = 0; ti < classTemplate.variables.length; ti++){
				var variable = classTemplate.variables[ti];
				bindings.bind(node, variable, dataContext);
			}
		},
		render: function(node, compilation, bindings, dataContext){
			var classTemplate = compilation.template;
			var rendered = classTemplate.render(bindings);
			node.element.className = rendered;
		}
	});

	aup.directive('forEach', {
		renderChildren: true,
		expression: ['$contextName', 'in', '$elemArray_VAR'], //!!!!!!!!!!!!!!!!!!!!!!!!!!
		bindings: function(node, compilation, bindings, dataContext){
			var elemArrayVar = compilation.elemArray;
			bindings.bind(node, elemArrayVar, dataContext);
		},
		render: function(node, compilation, bindings, dataContext){
			var contextName = compilation.contextName;
			
			var elemArrayVar = compilation.elemArray;
			
			/*
			 * 
			 * IL BIND DEVE ESSERE STERNO DA QUI? CHE FACCIO BINDO TANTE FUNZIONI?
			 * 
			 * 
			 */
			var elemArray = bindings.get(elemArrayVar);
			var times = elemArray.length;
			
			
			if(!node.hasData('originalChildren')) {
				node.data('originalChildren', node.children());
			}
			var children = node.data('originalChildren');
			node.empty();
			
			for(var ti = 0; ti < times; ti++){
				
				var currentObj = elemArray[ti]
				
				var extras = {};
				extras[contextName] = currentObj;
				var childBindings = bindings.createChild(extras);
				
				dataContext.put('index', ti);
				
				for ( var ci = 0; ci < children.length; ci++) {
					var child = children[ci];
					var childClone = child.clone();
					aup.util.render(childClone, childBindings, dataContext.forChild(childClone));
					
					node.append(childClone);
					
				}
				dataContext.remove('index');
				
			}
			
		}
	});
	aup.directive('with', {
		renderChildren: true,
		expression: ['$withVar_VAR', 'as', '$contextName'], //!!!!!!!!!!!!!!!!!!!!!!!!!!
		bindings: function(node, compilation, bindings, dataContext){
			var withVariable = compilation.withVar;
			bindings.bind(node, withVariable, dataContext);
		},
		render: function(node, compilation, bindings, dataContext){
			
			var newContextName = compilation.contextName;
			var withVariable = compilation.withVar;
			
			var value = bindings.get(withVariable);
			
			var extras = {};
			extras[newContextName] = value;
			var childBindings = bindings.createChild(extras);
			
			dataContext.put(newContextName, value);
			
			var children = node.children();
			for ( var ci = 0; ci < children.length; ci++) {
				var child = children[ci];
				aup.util.render(child, childBindings, dataContext.forChild(child));
			}
			
			dataContext.remove(newContextName);
		},
	});
	aup.directive('events', {
		compile: function(expression){
			var compilation = {};
			compilation.events = [];
			var executions = expression.split(',');
			for (var i = 0; i < executions.length; i++) {
				var execution = executions[i];
				var splitExec = execution.split(':');
				
				var command = aup.util.trim(splitExec[0]);
				var param = aup.variable(aup.util.trim(splitExec[1]));
				
				compilation.events.push({event: command, func: param});
			}
			return compilation
		},
		render: function(node, compilation, bindings, dataContext){
			var applyEvents = node.data('applyEvents');
			var events = compilation.events;
			if(applyEvents!=false && events != null){
				for(var ei = 0; ei < events.length; ei++){
					var event = events[ei];
					
					var eventName = event.event;
					var func = bindings.get(event.func);
					node.on(eventName, function(evt){
						var data = node.data('data');
						
						var eventReturn = func(data);
						
						if (eventReturn == false) {
							if (evt.preventDefault) {
								evt.preventDefault();
							} else {
								evt.returnValue = false;
							}
						}
						
					});
				}
				
				node.data('applyEvents', false);
				
			}
		},
	});
	
	aup.directive('field', {
		render: function(node, variable, bindings, dataContext){
			node.on('keyup', function(){
				bindings.set(variable, node.fieldValue());
			});
		}
	});
}

aup.util.compile = function(node) {
	if(node.type == 1){ // ELEMENT
		
		var directives = [];
		
		for(var name in aup.util.directives){
			
			var attr = node.attr(name);
			if(attr == null) continue;
			var directive = aup.util.directives[name];
			
			var data;
			if(directive.compile){
				data = directive.compile(attr);
			}else 
			if(directive.expression) {
				data = new TemplateExpression(directive.expression).match(attr);
				if(!data) throw new Error('Invalid expression: ' + attr);
			} else data = aup.util.parseVariable(attr);
			
			directives.push({compilation: data, directive: directive});
			
		}
		
		// CLASSES 
		// CORRETTO... PARSA SEMPRE???
		var classTemplate = aup.util.parse(node.element.className);
		if(classTemplate.variables.length > 0){
			directives.push({compilation:{template:classTemplate}, directive: aup.util.internal_directives['internal_class']});	
		}
		
		// ATTRIBUTES
		for(var ani = 0; ani <aup.util.templateAttributes.length; ani++){
			var attrName = aup.util.templateAttributes[ani];
			var attrValue = node.attr(attrName);
			if(attrValue != null){
				var attrTemplate = aup.util.parse(attrValue);
				if(attrTemplate.variables.length > 0){

					directives.push({compilation:{attr:attrName, template: attrTemplate}, directive: aup.util.internal_directives['internal_attr']});	
				}	
			}
		}	
		node.data('directives', directives);
		node.data('data', {});
		
		var children = node.childNodes();
		for ( var ci = 0; ci < children.length; ci++) {
			aup.util.compile(children[ci]);
		}	
		
		return;
	}
	
	if(node.type == 3){ // TEXT
		var text = node.text();
		var template = aup.util.parse(text);
		// CORRETTO... PARSA SEMPRE???
		if(template.variables.length > 0){
			node.data('directives',  [{compilation:{template:template}, directive: aup.util.internal_directives['internal_text']}]);
			node.data('data', {});
			
		} 
		
		return;
	}
	
};

aup.util.render = function(node, bindings, dataContext){
	var renderChildren = false;
	
	var directives = node.data('directives');
	
	if(directives){
		
		if(!node.hasData('applyBindings')){
			for(var di = 0; di < directives.length; di++){
				
				var directive = directives[di].directive;
				var compilation = directives[di].compilation;
				
				if(directive.bindings) directive.bindings(node, compilation, bindings, dataContext);
			}
			node.data('applyBindings', true);
		}
		
		for(var di = 0; di < directives.length; di++){
			var directive = directives[di].directive;
			var compilation = directives[di].compilation;
			
			if(directive.renderChildren) renderChildren = renderChildren || directive.renderChildren;
			
			if(directive.render) directive.render(node, compilation, bindings, dataContext);
		}
		
	}
	
		
	if(node.type == 1){ // ONLY ON ELEMENTS
		
		if(renderChildren == false) {
			var children = node.childNodes();
			for ( var ci = 0; ci < children.length; ci++) {
				aup.util.render(children[ci], bindings, dataContext);
			}	
		}
		
		
	}
	
	
}

aup.util.parse = function(value) {
	var variables = [];
	var varCheck = [];
	
	var tokenType = 'text';
	var token = undefined;
	
	var tokens = [];
	
	var collectToken = function(){
		if(!token) return;
		if(tokenType == 'text') tokens.push({type: 'text', value: token }); 
		else if(tokenType == 'variable') {
			var variable = aup.util.parseVariable(token);
			tokens.push({type: 'variable', value: variable});
			if(!varCheck[token]){
				varCheck[token] ={};
				variables.push(variable);
			}
		} else if(tokenType == 'function') {
			var func = aup.util.parseFunction(token);
			for(var vi = 0; vi < func.variables.length; vi++){
				var variable = func.variables[vi]; 
				if(!varCheck[variable]){
					varCheck[variable] ={};
					variables.push(variable);
				}
			}
			tokens.push({type: 'function', value: func });
			
		}
		
		token = '';
		tokenType = 'text';
	}
	
	for(var pi = 0; pi < value.length; pi++){
		
		var currentChar = value.charAt(pi);
		var ccNext = value.charAt(pi+1);
		
		if(currentChar == '{'){
			collectToken();
			tokenType = 'function';
			continue;
		}
		if(currentChar == '$' && ccNext == '{'){
			collectToken();
			tokenType = 'variable';
			pi++;
			continue;
		}
		if(currentChar == '}'){
			collectToken();
			continue;
		}
		
		if(!token) token = '';
		token += currentChar;
	}
	
	return new Template(variables, tokens, value);
};
aup.util.parseVariable = function(variable) {
	var contextName = undefined;
	var name = undefined;
	
	var token = '';
	for(var pi = 0; pi < variable.length; pi++){
		
		var currentChar = variable.charAt(pi);
		
		if(currentChar == '.'){
			if(token){
				contextName = token;
				token = '';
			}
			continue;
		}
		
		token += currentChar;
	}
	if(!name) name = token;
	
	return new TemplateVariable(contextName, name);
};
aup.util.parseFunction = function(functionText) {
	var specials = ' ()+-*/%=!&|';
	
	var variablesDeclaration = '';
	var rewrittenFunctionText ='\treturn ';
	
	var variables = [];
	var varCheck = {};
	var args = [];
	
	var token;
	
	var counter = 0;
	for(var pi = 0; pi < functionText.length; pi++){
		
		var currentChar = functionText.charAt(pi);
		
		if(specials.indexOf(currentChar) != -1) {
			if(token) {
				if(aup.util.isNumber(token) || token == 'true' || token == 'false'){
					rewrittenFunctionText += token;
					
				} else
				if(varCheck[token] === undefined){
					varCheck[token] = counter;
					variables.push(aup.util.parseVariable(token));
					rewrittenFunctionText += ('p' + counter);
					variablesDeclaration += '\tvar p' + counter + ' = ' + 'params[' + counter + '];\n';
					counter++;
				} else {
					var oldCounter = varCheck[token];
					rewrittenFunctionText += ('p' + oldCounter);
				}
				
				
				token = undefined;
			}
			rewrittenFunctionText += currentChar;
			
		} else {
			if(!token) token = '';
			token += currentChar;
		}
		

	}
	if(token) rewrittenFunctionText += token;
		
	
	var func = new Function('params', variablesDeclaration + rewrittenFunctionText);
	return new TemplateFunction(variables, func);
};


aup.util.calcValue = function(obj){
	if(aup.util.isFunction(obj)){
		return obj();
	}
	return obj;	
}



//WATCH
aup.watch = function(obj, propName, handler) {
	var val = obj[propName];
	
	// ARRAY
	if (aup.util.isArray(val)) {
		aup.util.watchArray(obj, propName, handler);
		return;
	}

	//OBJECT
	aup.util.watchObj(obj, propName, handler);

}

aup.util.watchObj = function(obj, propName, handler) {
	var val = obj[propName];
	var getter = function() {
		return val;
	};
	var setter = function(newVal) {
		val = newVal;
		
		var change = { set : {oldValue : val, newValue : newVal} };
		
		handler(change);
	};

	try {

		Object.observe(obj[propName], function(data) {
			setter(data);
		});

	} catch (e) {
		try {
			Object.defineProperty(obj, propName, {
				get : getter,
				set : setter,
				enumerable : true,
				configurable : true
			});
		} catch (e2) {
			try {
				Object.prototype.__defineGetter__.call(obj, propName, getter);
				Object.prototype.__defineSetter__.call(obj, propName, setter);
			} catch (e3) {
				throw new Error("aup watch not supported");
			}
		}

	}
}
aup.util.watchArray = function(obj, propName, handler) {
	var val = obj[propName];
	
	var methodNames = [ 'pop', 'push', 'reverse', 'shift', 'sort', 'slice',
			'unshift' ];
	for ( var mi = methodNames.length, methodName; mi--;) {
		methodName = methodNames[mi];
		aup.util.watchfunc(val, methodName, handler);
	}
	for(var waio = 0; waio < val.length; waio++){
		var arrayVal = val[waio];
		if (aup.util.isArray(arrayVal)) {
			aup.util.watchArray(val, waio, handler);
			
		} else{
			aup.util.watchObj(val, waio, handler);
		}

	}
	aup.util.watchObj(obj, propName, handler);
}
aup.util.watchfunc = function(obj, propName, methodHandler) {
	try {
		var originalFunc = obj[propName];
		Object.defineProperty(obj, propName, {
			enumerable : false,
			configurable : true,
			writable : false,
			value : function() {
				var res = originalFunc.apply(obj, arguments);
				var overrideRes = methodHandler(obj);
				if (overrideRes)
					return overrideRes;
				else
					return res;
			}
		});
	} catch (e) {
		throw new Error("aup watch not supported");
	}
}
